iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0
Modern Web

與 AI 一起開發 Side Project 吧!系列 第 17

Day17 — 穩如泰山 | 測試現在才寫,來得及嗎?

  • 分享至 

  • xImage
  •  

前言

為什麼要寫測試?

用 AI 幫忙寫程式,更需要寫測試。因為你不知道接下來 AI 輔助生成的程式碼,會把舊有的程式碼改成怎樣,會不會改得面目全非?

先前的經驗是被 AI 改過好幾次,對每次的結果都已經開始半信半疑,會逐行檢查 AI 修改的結果。而且我們開發者都有可能把「程式改壞」了,更別說 AI 會不會一不小心就改壞了。

所以說,基本功能雛形完成得差不多,可以預想接下來功能與業務邏輯越來越複雜,我們需要自動化測試的幫助,來把寫好的功能「保護」起來,這樣後續的功能才能更安心地開發,放心地與 AI 協作。

為什麼這時候才寫?

因為想寫先功能,先請 AI 幫我打造雛形,做出能看得見、能摸得著的初始產品,快速試用快速迭代。等確定功能與配置都修改到滿意之後,再來加上測試,才不會說試用了覺得不太對,程式碼一大部分打掉重練,先前寫的測試都白寫了…。

*此部分當然不是說 TDD(測試驅動開發) 不需要或不好用,而是今天的案例比較像是「需求驅動開發」,先寫功能再來做測試,比較合適我自己的 Side Project 開發。

試試看

直接下指令

聽說 AI 很厲害可以直接生成測試項目,那應該有辦法參考檔案,直接寫出我要的測試吧?

我直接下了「@AccountingCalculatorMain.tsx as ref, make unit test」

AI 咻咻咻的就產出了以下程式碼的測試,提供其中兩個供大家參考,體會一下。

it('renders two components with total amount as $0', () => {
  render(<App />);
  const totalElements = screen.getAllByText('$0');
  expect(totalElements).toHaveLength(2);
});

it('allows input of numbers and updates the display', () => {
  render(<App />);
  fireEvent.click(screen.getByText('1'));
  fireEvent.click(screen.getByText('2'));
  fireEvent.click(screen.getByText('3'));
  expect(screen.getByText('$123')).toBeInTheDocument();
});

//...

哇! 看起來有測到我想測試的,像是按鍵行為的測試,基本渲染的測試…等,都有測到耶!AI 自己幫我生成了這些測試項目:

https://ithelp.ithome.com.tw/upload/images/20240927/20168308CVXQAHCBGD.png

寫是寫出來了…但

等 AI 咻咻咻產生了所有測試之後,興高采烈地按下了 test 的按鈕。

https://ithelp.ithome.com.tw/upload/images/20240927/201683086DfgmhsOsM.jpg

幫我寫了 7 個測試,有 3 個是壞的,搞毛啊!

https://ithelp.ithome.com.tw/upload/images/20240927/201683087nyCLUwO8e.jpg

果然不能直接信任 AI 寫的測試,定睛一看錯誤的原因,是一些「比較低級」的錯誤,在於裏面的 screen 相關方法有點問題,以下條列幾個這次 AI 給的測試所發現的問題:

  • Found multiple elements… 出錯,是因為測驗中的組件都是以 screen.getByText()取得,這方法適用於「找到一個符合條件的組件」,但有些組件是「多個存在」的,所以就直接報錯。
  • 至於 Unable to find an element with the text: 刪除,是因為screen.getByText(’刪除’) 報錯,說是找不到這樣的組件。

就是以上這 2 個問題而已,看起來不是什麼大問題,等等來調整一下 :)

調整一下

首先是 screen.getByText() 發生的錯誤,測試原文是這樣的

test('renders the initial total amount as $0', () => {
  render(<App />);
  const totalElement = screen.getByText('$0');
  expect(totalElement).toBeInTheDocument();
});

乍看之下沒什麼問題,但對比一下畫面以及回想一下程式邏輯,在「尚未輸入」的前提之下,應該會有「2 個」$0 才對,如以下畫面所示

https://ithelp.ithome.com.tw/upload/images/20240927/20168308bcJxaxwrRn.png

所以這個測試項目,應該要改成「可以找到 2 個 $0 的組件」,一樣用指令告訴 AI 應該這樣修改:

測驗目標改為:可以找到 2 個 $0 的組件

AI 幫我改成這樣:

test('renders two components with total amount as $0', () => {
  render(<App />);
  const totalElements = screen.getAllByText('$0');
  expect(totalElements).toHaveLength(2);
});

嗯…看起來是對了,測試也通過沒問題 👍。至於另外一個 screen.getByText() 出錯的測試,依樣畫葫蘆修改一番即可。

另一個錯誤,光看測試好像沒什麼問題,但…為何會出錯呢?

test('deletes item from history when delete button is clicked', async () => {
  render(<App />);
  fireEvent.click(screen.getByText('1'));
  fireEvent.click(screen.getByText('0'));
  fireEvent.click(screen.getByText('OK'));
  fireEvent.click(screen.getByText('飲食'));
  
  const deleteButton = screen.getByText('刪除');
  fireEvent.click(deleteButton);
  
  expect(screen.queryByText('飲食')).not.toBeInTheDocument();
  expect(screen.queryByText('$10')).not.toBeInTheDocument();
});

再次操作一下實際的記帳 App 看看,發現原來是 AI 在「飲食」這裡 click 之後,並沒有再次按下「OK」,因此沒有將這筆帳目成功添加,也難怪「刪除」不會出現在畫面上。

這個就不用勞煩 AI 修改了,自個兒把上面的fireEvent.click(screen.getByText('OK')) 複製下來便行!再次跑了測試,嗯! 統統通過,可喜可賀 🥂

https://ithelp.ithome.com.tw/upload/images/20240927/201683081MjVUROKmF.png

結語

接下來,寫什麼測試來著?

雖然 AI 產出的測試項目姑且算是滿意,已經涵蓋許多記帳 App 的主要功能,但這就像是老闆直接拿著需求,請工程師「通靈」寫出程式碼一樣,終究是不太可靠,且不一定測試有覆蓋到真正重要的邏輯。

接著會介紹如何精準撰寫「最需要測」的測試,而不是任憑 AI 猜測所擺佈,尤其像是比較重要的「編輯、列表數量驗證…」等,都還沒有測到,接下來會再補上指令,將測試涵蓋主要使用者故事更為完整囉!


上一篇
Day16 — 馬不停蹄 | 很久很久以前…,忘了之前寫的東西了嗎? 沒關係,AI 講故事給你聽
系列文
與 AI 一起開發 Side Project 吧!17
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言